lib/WebSocketClient.ps1
Function Start-TMConsoleWebSocketClient { [CmdletBinding()] param ( [Parameter(Mandatory = $True)] [String] $Hostname, [Parameter(Mandatory = $True)] [Int] $Port, [Parameter()]$HostPID = -1, [Parameter(mandatory = $false)] [bool]$OutputVerbose = $false, ## PROD SETTING # [bool]$OutputVerbose = $true, ## DEV SETTING [Parameter()][Bool]$AllowInsecureSSL = $False, ## Use if debugging. This bypasses running the ActionRequest in an RS Job, and runs it locally instead. [Parameter()][Bool]$AllowDirectExecution = $False ) begin { ## Enable Verbose Output if OutputVerbose was true if ($OutputVerbose) { $global:VerbosePreference = 'Continue' $VerbosePreference = 'Continue' ## Write to Output if ($OutputVerbose) { Write-Output "Starting PowerShell Web Socket Client" } } ## ## Create Event Handlers for Runspace Management ## ## ## WebSocket Event Hander Definition: Send Data to Web Socket Server $EventHandler_WebSocket_SendData = [scriptblock] { param($WebSocketClient, $ClientId, $Queues) ## Write to Output if ($OutputVerbose) { Write-Output "Starting EventHandler_WebSocket_SendData" } $CancellationToken = New-Object Threading.CancellationToken($false) $WorkItem = $null while ($WebSocketClient.State -eq [Net.WebSockets.WebSocketState]::Open) { Start-Sleep -Milliseconds 500 while ($Queues.WebSocketClientSend.TryDequeue([ref] $WorkItem)) { ## Write to Output if ($OutputVerbose) { Write-Output "EventHandler_WebSocket_SendData Sending: $WorkItem" } [ArraySegment[byte]]$Message = [Text.Encoding]::UTF8.GetBytes($WorkItem) $WebSocketClient.SendAsync( $Message, [System.Net.WebSockets.WebSocketMessageType]::Binary, $true, $CancellationToken ).GetAwaiter().GetResult() | Out-Null } } ## Write to Output if ($OutputVerbose) { Write-Output "Ending EventHandler_WebSocket_SendData" } } ## ## WebSocket Event Hander Definition: Send Data to Web Socket Server $EventHandler_WebSocket_ReceiveData = [scriptblock] { param($WebSocketClient, $CancellationToken, $ClientId, $Queues) ## Write to Output if ($OutputVerbose) { Write-Verbose "Starting EventHandler_WebSocket_ReceiveData" } # ## Handle Receiving Data from the WebSocket $Size = 32768 $Array = [byte[]] @(, 0) * $Size $ReceiveBuffer = New-Object System.ArraySegment[byte] -ArgumentList @(, $Array) While ($WebSocketClient.State -eq 'Open') { try { $MessageString = "" Do { ## Create an Event to receive the next Buffer $ReceiveDataTask = $WebSocketClient.ReceiveAsync($ReceiveBuffer, $CancellationToken) While (-Not $ReceiveDataTask.IsCompleted) { Start-Sleep -Milliseconds 250 } $ReceiveBuffer.Array[0..($ReceiveDataTask.Result.Count - 1)] | ForEach-Object { $MessageString = $MessageString + [char]$_ } } Until ($ReceiveDataTask.Result.Count -lt $Size) ## There was a message delivered from TMConsole if ($MessageString) { ## Write to Output if ($OutputVerbose) { Write-Output "WebSocket Received a Message, Queueing for SessionManager: $MessageString" } ## Queue the Message for the SessionManager to handle $Queues.SessionManager.Enqueue($MessageString) } } catch { $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'Debug THROWN ERROR' Error = $_ } | ConvertTo-Json -Depth 5)) Write-Host $_ } } ## Write to Output if ($OutputVerbose) { Write-Output "Ending EventHandler_WebSocket_ReceiveData" } } ## ## Runspace Event Handler - Remove Completed Runspace Jobs $ScriptBlock_TaskRunspace_Remove_Completed = [scriptblock] { param($TMTaskId) ## Write to Output if ($OutputVerbose) { Write-Output "Running ScriptBlock_TaskRunspace_Remove_Completed for $TMTaskId" } Unregister-Event -SourceIdentifier ($TMTaskId + "_Runspace_StateChanged") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Information") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Progress") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Error") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Verbose") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Debug") Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Warning") ## Remove the Runspace Job try { $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' } Remove-RSJob -Job $RSJob -Force -ErrorAction SilentlyContinue } catch { $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'SystemError' From = 'Removing Runspace' Detail = 'RSJob_RemoveCompleted FAILED removing the RSJob' Error = $_.Exception.Message } | ConvertTo-Json)) } ## Report current status of Runspace Jobs $RunningRSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' } $NewStatus = @{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $Hostname serverStatus = "Monitoring $($RunningRSJobs.Count) TMC Actions" from = 'ScriptBlock_TaskRunspace_InvokeActionRequest' } } $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) ## Write to Output if ($OutputVerbose) { Write-Output "Ending ScriptBlock_TaskRunspace_Remove_Completed for $TMTaskId" } } ## ## Event Hander Definitions: Invoke Action Requests $ScriptBlock_TaskRunspace_InvokeActionRequest = [scriptblock] { param ( [Parameter()] [PSObject] $ActionRequest, [Parameter()] [System.Boolean] $AllowInsecureSSL = $False, [Parameter()] [System.Boolean] $AllowDirectExecution = $False, $Queues, $WebSocketClient ) ## Write to Output if ($OutputVerbose) { Write-Output "Starting ScriptBlock_TaskRunspace_InvokeActionRequest" } ## Ensure all streams are enabled, but Errors stop $InformationPreference = 'Continue' $VerbosePreference = 'Continue' $ProgressPreference = 'Continue' $DebugPreference = 'Continue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Continue' ## Rename the Variable to make the invocation logic more clear $TMTaskID = 'TMTaskId_' + [string]$ActionRequest.task.id ## Send a set of Startup Messages $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $TMTaskId Type = 'TaskStarted' }, [PSCustomObject]@{ TMTaskId = $TMTaskID Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Queued Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title CurrentOperation = '' StatusDescription = '' PercentComplete = 0 SecondsRemaining = -1 RecordType = 0 } } ) ## Send the StatusMessages to TMConsole $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress)) # Allow Running Serially for Debugging if ($AllowDirectExecution) { ## Write to Output if ($OutputVerbose) { Write-Output "Starting Scriptblock Direct Execution" } ## Prepare the Single-Threaded Invocation options $InvokeCommandParams = @{ ArgumentList = @($ActionRequest, $AllowInsecureSSL) ScriptBlock = $ActionRequestInnerJobScriptBlock } try { Invoke-Command @InvokeCommandParams } catch { throw $_.Exception.Message } return } ## Else (Not needed because of the return above) ## Prepare the Runspace Job Options ## and start the Runspace Job try { ## Write to Output if ($OutputVerbose) { Write-Output "Sending WebSocket Message and Starting Runspace Job" } ## Send the StatusMessages to TMConsole $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress)) ## Create an appropriate Runspace Name $TaskJobName = [String]([string]$TMTaskID + "_" + (Get-Date -Format 'FileDateTimeUniversal')) # ## Before Starting the RSJob, Make sure the Provider Module is imported into # ## this TMD session so it's loaded and available to supply to any future Provider Tasks # ## Include TMD and TM, and add any provider modules $ModulesToImport = @('TMConsole.Client', 'TMD.Common', 'TransitionManager') $JobParams = @{ Name = $TaskJobName ArgumentList = @($ActionRequest, $AllowInsecureSSL) ModulesToImport = $ModulesToImport ScriptBlock = $ActionRequestInnerJobScriptBlock } ## Start the RS Job $RSJob = Start-RSJob @JobParams ## Send a set of Started Up Messages $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $TMTaskID Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Running Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title CurrentOperation = 'Running' StatusDescription = '' PercentComplete = 5 SecondsRemaining = -1 RecordType = 0 } } ) ## Send the StatusMessages to TMConsole $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress)) ## ## RS Job Output Event Handlers ## ## Create a Common Streams Output Event Splat ## Used for Info, Debug, Progress, Error, Verbose and Warning $StreamsObjectEventSplat = @{ EventName = 'DataAdded' Action = $EventHandler_TaskRunspace_Streams MessageData = @{ TMTaskId = $TMTaskID Queues = $Queues } } ## Create an event for Information Stream Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Information SourceIdentifier = ($TMTaskID + "_Stream_Information") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Stream Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Progress SourceIdentifier = ($TMTaskID + "_Stream_Progress") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Error Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Error SourceIdentifier = ($TMTaskID + "_Stream_Error") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Verbose Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Verbose SourceIdentifier = ($TMTaskID + "_Stream_Verbose") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Debug Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Debug SourceIdentifier = ($TMTaskID + "_Stream_Debug") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Create an event for Progress Warning Output $ActivitySplat = @{ InputObject = $RSJob.InnerJob.Streams.Warning SourceIdentifier = ($TMTaskID + "_Stream_Warning") } [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat) ## Register Startup and Output events $RunspaceStateChangedEventSplat = @{ SourceIdentifier = ($TMTaskID + "_Runspace_StateChanged") EventName = 'InvocationStateChanged' InputObject = $RSJob.InnerJob Action = $EventHandler_TaskRunspace_StateChanged MessageData = @{ TMTaskId = $TMTaskID Queues = $Queues } } [void](Register-ObjectEvent @RunspaceStateChangedEventSplat) ## ## Update the Action Counter in the UI ## ## Get the list of RS Jobs in progress to report Session Manager Status $RSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' } ## Update PowershellServerStatus $NewStatus = @{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $Hostname serverStatus = "Monitoring $($RSJobs.Count) TMC Actions" from = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Started New Action' } } $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) } catch { $NewStatus = @{ Type = 'SystemError' From = 'Starting ActionRequest RunspaceJob' Message = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error' Exception = $_.Exception.Message } $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) } ## Write to Output if ($OutputVerbose) { Write-Output "Ending ScriptBlock_TaskRunspace_InvokeActionRequest" } } ## ## Event Hander Definitions: Task Runspace State Change $EventHandler_TaskRunspace_StateChanged = [scriptblock] { ## Write to Output if ($OutputVerbose) { Write-Output "Starting EventHandler_TaskRunspace_StateChanged" } ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId $Queues = $Event.MessageData.Queues ## Assign the Stream ID based on a possible redirection $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId ## Process each Event Item $Event.SourceArgs | ForEach-Object { ## Name the variable for convenience $NewData = $_ $NewDataType = $NewData.GetType().ToString() ## The type of Raised Event determines what to do switch ($NewDataType) { ## Handle a PowerShell (session) object 'System.Management.Automation.PowerShell' { ## Get the Job to determine if there's more data $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' } ## Write to Output if ($OutputVerbose) { Write-Output "EventHandler_TaskRunspace_StateChanged -- State Change: $RSJob" } ## Switch on the Invocation State switch ($NewData.InvocationStateInfo.State.ToString()) { 'Completed' { # if ($RSJob.HasMoreData) { # $Output = $RSJob | Receive-RSJob # $Output | ConvertTo-Json -Depth 5 -Compress | Write-Host -ForegroundColor green # } # Send a Progress Activity $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Task Completed' CurrentOperation = 'Complete' StatusDescription = '' PercentComplete = 100 SecondsRemaining = -1 RecordType = 1 } }, [PSCustomObject]@{ TMTaskId = $StreamId Type = 'TaskCompleted' RSJobName = $RSJob.Name } ) ## Send the StatusMessages to TMConsole $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress)) ## SessionManager message $SessionManagerMessage = @{ TMTaskId = $TMTaskId Type = "RemoveRunSpace" } | ConvertTo-Json $Queues.SessionManager.Enqueue($SessionManagerMessage) } 'Failed' { ## Capture the error message to send to TMConsole $InvocationError = $NewData.InvocationStateInfo.Reason.ErrorRecord.Exception.Message # Send a Progress Activity $StatusMessages = @( [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Error' Message = $InvocationError }, [PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = [PSCustomObject]@{ ActivityId = 0 ParentActivityId = -1 Activity = 'Task Failed' CurrentOperation = 'Failed' StatusDescription = '' PercentComplete = 100 SecondsRemaining = -1 RecordType = 2 } }, [PSCustomObject]@{ TMTaskId = $StreamId Type = 'TaskFailed' Message = $InvocationError } ) ## Send the StatusMessages to TMConsole $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress)) ## SessionManager message $SessionManagerMessage = @{ TMTaskId = $TMTaskId Type = "RemoveRunSpace" } | ConvertTo-Json $Queues.SessionManager.Enqueue($SessionManagerMessage) } Default { if ($OutputVerbose) { Write-Output "TMTask: $TMTaskId has some other state: $($NewData.InvocationState.State.ToString())" } } } break } # Handle a PSInvocationStateChangedEventArgs 'System.Management.Automation.PSInvocationStateChangedEventArgs' { ## This State Changed Object is Redundent. The 'PowerShell' object that is also passed ## Contains all of the information needed and this Event Args can safely be ignored break } ## Handle anything that wasn't a known type Default { Write-Host "Received an unhandled Object!! $($NewData.GetType().ToString())" -ForegroundColor Red Write-Host "`t$($NewData | ConvertTo-Json -EnumsAsStrings -Depth 3)" -ForegroundColor Red } } } ## Write to Output if ($OutputVerbose) { Write-Output "Ending EventHandler_TaskRunspace_StateChanged" } } ## ## Event Hander Definitions: StreamOutput from Task Runspaces $EventHandler_TaskRunspace_Streams = [scriptblock] { ## Write to Output if ($OutputVerbose) { Write-Output "Starting EventHandler_TaskRunspace_Streams" } ## Collect the TMTaskId from the MessageData $TMTaskId = $Event.MessageData.TMTaskId $Queues = $Event.MessageData.Queues ## Assign the Stream ID based on a possible redirection $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId ## Create a Messages Arrays to store the incoming messages in $MessagesToProcess = [System.Collections.ArrayList]::new() $MessagesToSend = [System.Collections.ArrayList]::new() ## ## Iterate to collect any SourceArgs items and move them to a Processing array ## This is done in this fashion to quickly collect the messages for later processing ## They are not processed and sent initally, because this function must also clear the stream ## If this is done too long after the messages come in, you risk clearing unprocessed items. ## ## Save each of the Messages delivered to a separate Array foreach ($NewData in $Event.SourceArgs[0]) { ## Move the NewData item into the Processing Queue Array [void]$MessagesToProcess.Add($NewData) } ## With the SourceArgs messages safely stored, ## Clear the Event Stream $Event.Sender.clear() ## ## Process each message, collecting Tokenized messages to send. ## Sending is not done one-at-a-time, but as an array so Angular ## Has the ability to process multiple messages before publishing an ## Observable update ## Process each Message waiting to be processed foreach ($NewData in $MessagesToProcess) { ## Switch based on the type of object in the stream switch ($NewData.GetType().ToString()) { ## Error Records 'System.Management.Automation.ErrorRecord' { ## Do nothing here because the Runspace Monitoring will pick up the error and supply ## it to TMConsole to be handled. break } ## Write-Progress Messages 'System.Management.Automation.ProgressRecord' { ## Ignore ActivityID -1 (Used by Invoke-WebRequest and others for temporary Progress Bars) if (` ($NewData.Activity -ne 'Reading web response')` -and ($NewData.ActivityId -ge 0) ` -and ($NewData.PercentComplete -ge 0)` ) { # Setup a Progress Message to send [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Progress' Message = @{ Activity = $NewData.Activity ActivityId = $NewData.ActivityID ParentActivityId = $NewData.ParentActivityId CurrentOperation = $NewData.CurrentOperation StatusDescription = $NewData.StatusDescription SecondsRemaining = $NewData.SecondsRemaining PercentComplete = $NewData.PercentComplete RecordType = $NewData.RecordType } } ) } break } ## Write-Verbose Messages 'System.Management.Automation.VerboseRecord' { [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Verbose' Message = $NewData.Message } ) break } ## Write-Warning Records 'System.Management.Automation.WarningRecord' { [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Warning' Message = $NewData.Message } ) break } ## Write-Debug Records 'System.Management.Automation.DebugRecord' { [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Debug' Message = $NewData.Message } ) break } <## Write-Host, Out-Host Records $NewData.MessageData is like @{ Message = 'Hello, World!' ForegroundColor = 'White' BackgroundColor = 'Black' NoNewLine = $True|$False } for standard 'Write-Host' output. Within the TMConsole.Client UI command set, there are other types of output that are plucked from this stream #> 'System.Management.Automation.InformationRecord' { ## TMConsole.Client commands may prefix output with code "||TMC:" to perform an alternate action ## These Write-Host output objects are a token with a type that is handled specifically by the TMConsole UI ## to display a beautiful component or to invoke some functionality within TMConsole. if (($NewData.MessageData.Message.Length -gt 5) -and ($NewData.MessageData.Message.substring(0, 6) -eq '||TMC:')) { # ## Trim the leading characters # $Message = $NewData.MessageData.Message -replace "\|\|TMC:", '' ## Convert the reaminder of the first line from JSON $TmcObject = ($Message -split "`n")[0] | ConvertFrom-Json $Queues.WebSocketClientSend.Enqueue(($DebugStatus | ConvertTo-Json -Compress)) ## Handle Different Types of TMCObjects switch ($TmcObject.Type) { ## Banners are created by Write-Banner from TMConsole.Client ## They will be displayed with a CSS styled banner componenet 'Banner' { $Queues.WebSocketClientSend.Enqueue( ([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Banner' Message = $TmcObject } | ConvertTo-Json -Compress) ) } <# BrokerUpdate messages are created when a Broker starts a Subject Task and when a Subject ends. BrokerUpdate = @{ Type = 'BrokerUpdate' TMTaskId = TMTaskId_{actionrequest.task.id} TargetStreamId = TMTaskId_{subjecttask.task.id} } The purpose of this message is 2 fold: 1 - to record a change to the target Task ID stream that should receive the console output When a broker first starts, the Stream ID is that of the Broker. This means console/progress output emitted by the Broker is displayed in the Progress/Console _for the broker_ task. When a BrokerUpdate provides an alternate StreamID, that stream then becomes the target Task ID. Any output received by the SessionManager will be streamed to the Subject Task ID. A BrokerUpdate is also received to return the Broker to 'normal', by supplying the TargetStreamId of the Broker Task. This restores the output stream to the Broker task, not the subject. 2 - When a BrokerUpdate has differing StreamIds (meaning it's output is redirected to a subject), this also indicates that the Subject Task should be displayed in TMConsole's Task List. When this occurs, the BrokerUpdate is forwarded to Angular, so it can caretake for ensuring that the Subject Task is then brought into the TaskList view. #> 'BrokerUpdate' { ## Calculate the TMTaskId_ for the Target Stream $SubjectId = 'TMTaskId_' + $TmcObject.SubjectTaskId ## If the BrokerStarting a Redirect if ($TmcObject.Change -eq 'StartRedirect') { $Global:TaskStreamRedirections.$TMTaskId = $SubjectId } ## The Broker is ending a Stream Redirection else { ## Get an existing Stream Redirection Record for the Broker task $TMTaskId if ($Global:TaskStreamRedirections.Keys -contains $TMTaskId) { [void]$Global:TaskStreamRedirections.Remove($TMTaskId) } } ## Create a message to TMC so the UI can add the Subject Task [void]$MessagesToSend.Add($TmcObject) } } } ## Plain Write-host InformationRecord objects Else { [void]$MessagesToSend.Add([PSCustomObject]@{ TMTaskId = $StreamId Type = 'Information' Message = $NewData.MessageData } ) } break } ## Error Records Default { if ($OutputVerbose) { Write-Output ('Unknown Data: ' + ($NewData | ConvertTo-Json)) } } } } ## ## Send any messages collected as one array object. ## # If there were any messages collected to send if ($MessagesToSend.Count -gt 0) { ## Convert the data to JSON $MessagesJSON = ($MessagesToSend | ConvertTo-Json -Depth 10 -Compress ) ## Send the Update to TMConsole $Queues.WebSocketClientSend.Enqueue($MessagesJSON) } ## Write to Output if ($OutputVerbose) { Write-Output "Ending EventHandler_TaskRunspace_Streams" } } ## ## Define ActionRequest-Task Executing Scriptblock that runs inside the RSJob ## ## Build up the proper runspace invocation that isn't otherwise working $ActionRequestInnerJobScriptBlock = [scriptblock] { param($ActionRequest, $AllowInsecureSSL) ## Write to Output if ($OutputVerbose) { Write-Output "Starting ActionRequestInnerJobScriptBlock" } ## Ensure all streams are enabled $InformationPreference = 'Continue' $VerbosePreference = 'Continue' $ProgressPreference = 'Continue' $DebugPreference = 'Continue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Continue' ## Sleep long enough to let the Event Handler attach Start-Sleep -Milliseconds 200 ## Write a Verbose Message starting the task Write-Verbose ('Starting Task TMTaskId_' + [string]$ActionRequest.task.taskNumber + ': ' + $ActionRequest.task.title) # ## Import the TMC Action Request, which also loads Provider Modules . Import-TMCActionRequest -PSObjectActionRequest $ActionRequest # ## Create a Parameters Variable from the Action Script New-Variable -Name Params -Scope Global -Value $ActionRequest.params -Force ## Add $Credential if there is one if ($ActionRequest.PSCredential) { New-Variable -Scope Global -Name Credential -Value $ActionRequest.PSCredential -Force } ## Invoke the User Script block $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script) ## Run the User Provided Script try { Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope } catch { throw $_ } ## Create a Data Options parameter for the Complete-TMTask command $CompleteTaskParameters = @{} ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion if ($Global:TMAssetUpdates) { $CompleteTaskParameters = @{ Data = @{ assetUpdates = $Global:TMAssetUpdates } } } ## Add SSL Exception if necessary if ($AllowInsecureSSL) { $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True } ## Complete the TM Task, sending Updated Data values for the task Asset if ($ActionRequest.HostPID -ne -1) { Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters } ## Write to Output if ($OutputVerbose) { Write-Output "Ending ActionRequestInnerJobScriptBlock" } } } ## Execute the Main Function of the Script Process { Try { ## Create the WebSocket Send Queue as a Synchronized Queue so Event Handlers can access it New-Variable -Name Queues -Force -Scope Global -Value @{ WebSocketClientSend = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]' WebSocketClientReceive = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]' SessionManager = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]' } # ## Queue the first message up # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # Type = 'Debug' # Detail = 'SessionManager is starting Process' # } | ConvertTo-Json)) Do { ## Create a Stream Redirection Cache New-Variable -Name TaskStreamRedirections -Force -Scope Global -Value @{ Robin = 'Streb' Pup = 'Atticus' } # Store the Configuration Details for this Remote Session (Web Socket Server Host) New-Variable -Name PowershellServerStatus -Force -Scope Global -Value @{ connectionStatus = 'Connecting' serverName = $Hostname serverStatus = 'Connecting' } # ## Make the Inital WebSocket and Connect # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # Type = 'Debug' # Detail = 'SessionManager Connecting to TMConsole WebSocket' # } | ConvertTo-Json)) ## Create Send and Receive queues for the Web Socket # [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} $ClientId = New-Guid | Select-Object -ExpandProperty Guid $WebSocketClient = New-Object System.Net.WebSockets.ClientWebSocket $CancellationToken = New-Object System.Threading.CancellationToken # $WebSocketClient.Options.RemoteCertificateValidationCallback = { return $true } # [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } # $handler = [System.Net.Http.HttpClientHandler]::new() # $ignoreCerts = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator # $handler.ServerCertificateCustomValidationCallback = $ignoreCerts ## Establish the connection and wait until it answers $VerbosePreference = 'Continue' $Global:VerbosePreference = 'Continue' ## Establish the WebSocket connection $WssEndpoint = "wss://$($Hostname):$($Port)" Write-Verbose "Connecting to WSS Endpoint: $WssEndpoint" $Connection = $WebSocketClient.ConnectAsync($WssEndpoint, $CancellationToken) While (!$Connection.IsCompleted) { Start-Sleep -Milliseconds 100 } if ($Connection.IsFaulted) { throw "Connection Faulted: $($Connection.Exception.InnerException.InnerException.InnerException.Message)" } ## Update PowershellServerStatus $NewStatus = @{ Type = 'powershell-server-status' Message = @{ connectionStatus = 'Connected' serverName = $Hostname serverStatus = "Monitoring 0 TMC Actions" from = 'WebSocket Client Connected' } } $global:Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress)) ## ## Start An Event Handler for the WebSocketClientSend Queue ## This caretakes to empty the 'SendQueue' that the Runspaces have access to ## # ## Start a WebSocket Sending Runspace # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # Type = 'Debug' # Detail = 'SessionManager is Creating a WebSocket Sending Runspace' # } | ConvertTo-Json)) $SendRunspace = [PowerShell]::Create() $SendRunspace.AddScript($EventHandler_WebSocket_SendData). AddParameter("WebSocketClient", $WebSocketClient). AddParameter("ClientId", $ClientId). AddParameter("Queues", $global:Queues).BeginInvoke() | Out-Null # ## Start a WebSocket Receiving Runspace # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # Type = 'Debug' # Detail = 'SessionManager is Creating a WebSocket Receiving Runspace' # } | ConvertTo-Json)) $ReceiveRunspace = [PowerShell]::Create() $ReceiveRunspace.AddScript($EventHandler_WebSocket_ReceiveData). AddParameter("WebSocketClient", $WebSocketClient). AddParameter("CancellationToken", $CancellationToken). AddParameter("ClientId", $ClientId). AddParameter("Queues", $global:Queues).BeginInvoke() | Out-Null # ## Start a WebSocket Receiving Runspace # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # Type = 'Debug' # Detail = 'SessionManager is Now waiting for for Instructions' # } | ConvertTo-Json)) ## Run a While Connection=Open loop While ($WebSocketClient.State -eq 'Open') { ## Sleep Start-Sleep -Milliseconds 250 $MessageString = "" if ($global:Queues.SessionManager.TryDequeue([ref]$MessageString)) { ## Convert the incoming String data to an Object $Message = $MessageString | ConvertFrom-Json $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'Debug' Detail = 'SessionManager received a message' MessageType = $Message.Type } | ConvertTo-Json)) ## Switch Activity based on the Type in the Message $InvokeSplat = @{} switch ($Message.Type) { 'ActionRequest' { $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ TMTaskId = 'TMTaskId_' + $Message.task.id Type = 'Debug' Detail = 'Invoking ActionRequest' } | ConvertTo-Json)) ## Run the Action Request $InvokeSplat = @{ ScriptBlock = $ScriptBlock_TaskRunspace_InvokeActionRequest ArgumentList = $Message, $AllowInsecureSSL, $AllowDirectExecution, $global:Queues, $WebSocketClient } try { Invoke-Command @InvokeSplat } catch { $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'SystemError' From = 'SessionManager Invoking ActionRequest' ErrorMessage = $_.Exception.Message StackTrace = $_.Exception.StackTrace ScriptName = $_.InvocationInfo.ScriptName ErrorLine = $_.InvocationInfo.ScriptLineNumber } | ConvertTo-Json -Depth 3)) } } 'RemoveRunspace' { # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ # TMTaskId = $Message.TMTaskId # Type = 'Debug' # Detail = 'Removing Runspace' # } | ConvertTo-Json)) ## Run the Action Request $InvokeSplat = @{ ScriptBlock = $ScriptBlock_TaskRunspace_Remove_Completed ArgumentList = $Message.TMTaskId } try { Invoke-Command @InvokeSplat } catch { $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{ Type = 'SystemError' From = 'SessionManager Invoking RemoveRunspace' ErrorMessage = $_.Exception.Message StackTrace = $_.Exception.StackTrace ScriptName = $_.InvocationInfo.ScriptName ErrorLine = $_.InvocationInfo.ScriptLineNumber } | ConvertTo-Json -Depth 3)) } } Default {} } } } Write-Verbose "WebSocketConnection is no longer open!" } Until (!$Connection) Write-Verbose "WebSocketConnection is now closed" } Catch { throw $_ } } End { Write-Verbose "TMConsole.WebSocketClient is ending" If ($WebSocketClient) { Write-Verbose "Closing websocket" $WebSocketClient.Dispose() } Write-Verbose "TMConsole.WebSocketClient has ended" } } |